Skip to content

[CLI-321] Add and use a Converter interface and implementations without using BeanUtils#216

Merged
garydgregory merged 25 commits into
apache:masterfrom
Claudenw:Functional_Converters
Jan 29, 2024
Merged

[CLI-321] Add and use a Converter interface and implementations without using BeanUtils#216
garydgregory merged 25 commits into
apache:masterfrom
Claudenw:Functional_Converters

Conversation

@Claudenw

@Claudenw Claudenw commented Jan 3, 2024

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/CLI-321

Similar to #215 but without using BeanUtils.

Implementation introduces Converter and Verifier interfaces.

Converter does as it says and converts from Str to object.
Verifier verifies that string values are proper before doing the conversion. Checks are made when the string is added to the values in the Option object.

TypeHandler is reworked to use and manage the mapping of the Converter and Verifier instances to Classes.
Methods are provided to register Converter and Verifier for a class.

Tests added for Converters and Verifiers as well as TypeHandler changes.

Changes to earlier tests:

There was a TypeHandler test that expected the value "3,5" to throw and exception when it was retrieved, that now throws an exception when the value is initially added to the Option during command line parsing. Test has been updated to account for this.

Some javadocs need to be completed.

Adds BeanUtils dependency to POM and implements TypeHandler using the BeanUtils classes.

Handles all methods that were in the original TypeHandler as well as other default classes provided by BeanUtils.
Test updated to show proper parsing of types that previously were not implemented.

Includes new cli.converters package that may be better handled by BeanUtils as it moves forward to support FunctionalInterfaces.
Added getParsedOptionValues methods with default values.
Fixed som javadoc,
@Claudenw Claudenw requested a review from garydgregory January 3, 2024 10:51
@Claudenw Claudenw marked this pull request as draft January 3, 2024 10:51
@codecov-commenter

codecov-commenter commented Jan 3, 2024

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.47423% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.01%. Comparing base (b3cdbea) to head (2d6ff50).
⚠️ Report is 574 commits behind head on master.

Files with missing lines Patch % Lines
.../main/java/org/apache/commons/cli/TypeHandler.java 76.59% 11 Missing ⚠️
.../main/java/org/apache/commons/cli/CommandLine.java 75.00% 3 Missing ⚠️
...in/java/org/apache/commons/cli/ParseException.java 71.42% 1 Missing and 1 partial ⚠️
...a/org/apache/commons/cli/PatternOptionBuilder.java 92.30% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #216      +/-   ##
============================================
- Coverage     93.20%   92.01%   -1.19%     
- Complexity      570      582      +12     
============================================
  Files            21       22       +1     
  Lines          1206     1253      +47     
  Branches        216      210       -6     
============================================
+ Hits           1124     1153      +29     
- Misses           46       63      +17     
- Partials         36       37       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Claudenw Claudenw mentioned this pull request Jan 3, 2024
@garydgregory

Copy link
Copy Markdown
Member

Hi @Claudenw
Thank you for the PR.
Run 'mvn' by itself before pushing to catch these build failures.

@Claudenw

Claudenw commented Jan 4, 2024

Copy link
Copy Markdown
Contributor Author

@garydgregory I think this might be ready for prime time now.

@Claudenw Claudenw self-assigned this Jan 4, 2024
@Claudenw Claudenw marked this pull request as ready for review January 4, 2024 10:57

@garydgregory garydgregory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @Claudenw
Thank you for this PR.
I think the best path is to work toward getting this PR in instead of the BeanUtils version.
Please see my comments, which are few, per my issue formatting.

* @since 1.7
*/
@FunctionalInterface
public interface Verifier {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably more flexible if this extends Predicate<String>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can get rid of Verifier and just use Predicate

@garydgregory garydgregory Jan 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the advantage of a custom interface here is that we are cementing the design to only test String input, as opposed CharSequence for example (or anything else). So we should make it clear at least in our mind what the intent is here or if there should be such an input-type restriction.

/**
* The definition of the functional interface to call when verifying a string
* for input Like {@code Predicate<String>} but can throw a RuntimeException.
* @since 1.7

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next feature version will be 1.7.0 (major.minor.maintenance).

Comment thread src/main/java/org/apache/commons/cli/CommandLine.java
*
* @param str the File location
* @return The file represented by {@code str}.
* @param str the File location

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert the formatting changes. It makes the PR noisier, larger and takes longer to review.

* @since 1.7
*/
@FunctionalInterface
public interface Converter<T> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should extend Function<String, T>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't since Converter throws a ParseException and Function<String,T> does not allow that.

@garydgregory garydgregory Jan 5, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we don't want to depend on Commons Lang's FailableException here.

Next: Throwing Exception is an anti-pattern and should be avoided at all costs IMO. Either:

  • reconsider and throw runtime exception OR
  • throw a more specific checked exception OR
  • parameterize the exception type

Integer.class);
checkOption(Option.builder().option("a").desc("desc").type(Integer.class).build(), "a", "desc", null, Option.UNINITIALIZED, null, false, false,
defaultSeparator, Integer.class);
checkOption(Option.builder("a").desc("desc").build(), "a", "desc", null, Option.UNINITIALIZED, null, false,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's changed here? If it's just formatting, please revert, I don't want to take the time to tease apart formatting from feature changes.

@garydgregory

Copy link
Copy Markdown
Member

How about adding support for Path?

@Claudenw

Claudenw commented Jan 5, 2024

Copy link
Copy Markdown
Contributor Author

@garydgregory Sorry about the formatting.

What would you want to see for Path?

I think that Verifier class should become a static class that simply contains static common Predicate instances.

Where Verifier is currently used in the code it will become Predicate .

I don't think there needs to be a difference. At one time I had the Verifier throwing the ParseException, but that has been fixed, so there is no valid distinction.

What are your thoughts?

I also want to move everything in o.a.c.cli.converters into o.a.c.cli

@garydgregory

Copy link
Copy Markdown
Member

@garydgregory Sorry about the formatting.

What would you want to see for Path?

I think that Verifier class should become a static class that simply contains static common Predicate instances.

Where Verifier is currently used in the code it will become Predicate .

I don't think there needs to be a difference. At one time I had the Verifier throwing the ParseException, but that has been fixed, so there is no valid distinction.

What are your thoughts?

I also want to move everything in o.a.c.cli.converters into o.a.c.cli

Path: feature parity with File.

@garydgregory

Copy link
Copy Markdown
Member

@garydgregory Sorry about the formatting.
What would you want to see for Path?
I think that Verifier class should become a static class that simply contains static common Predicate instances.
Where Verifier is currently used in the code it will become Predicate .
I don't think there needs to be a difference. At one time I had the Verifier throwing the ParseException, but that has been fixed, so there is no valid distinction.
What are your thoughts?
I also want to move everything in o.a.c.cli.converters into o.a.c.cli

Path: feature parity with File.

Which can be done later.

@garydgregory

garydgregory commented Jan 6, 2024

Copy link
Copy Markdown
Member

Hi @Claudenw

Hm, I think this PR is overly complicated.

Specifically, I do not think there should be a split between verifying and converting, there should not be a whole Verifier infrastructure IMO: The converters do their parsing, and in this PR, the syntaxes between verifying and parsing don't match anyway.

For a simple example, it is legal for an Integer to have a leading + character but the regular expression for integers doesn't allow it.

It's worse if I want to have a Double parameter since the RE for that is large and complex as documented in Double.valueOf(String) and it's certainly not what this PR does:

     *  final String Digits     = "(\\p{Digit}+)";
     *  final String HexDigits  = "(\\p{XDigit}+)";
     *  // an exponent is 'e' or 'E' followed by an optionally
     *  // signed decimal integer.
     *  final String Exp        = "[eE][+-]?"+Digits;
     *  final String fpRegex    =
     *      ("[\\x00-\\x20]*"+  // Optional leading "whitespace"
     *       "[+-]?(" + // Optional sign character
     *       "NaN|" +           // "NaN" string
     *       "Infinity|" +      // "Infinity" string
     *
     *       // A decimal floating-point string representing a finite positive
     *       // number without a leading sign has at most five basic pieces:
     *       // Digits . Digits ExponentPart FloatTypeSuffix
     *       //
     *       // Since this method allows integer-only strings as input
     *       // in addition to strings of floating-point literals, the
     *       // two sub-patterns below are simplifications of the grammar
     *       // productions from section 3.10.2 of
     *       // The Java Language Specification.
     *
     *       // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt
     *       "((("+Digits+"(\\.)?("+Digits+"?)("+Exp+")?)|"+
     *
     *       // . Digits ExponentPart_opt FloatTypeSuffix_opt
     *       "(\\.("+Digits+")("+Exp+")?)|"+
     *
     *       // Hexadecimal strings
     *       "((" +
     *        // 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt
     *        "(0[xX]" + HexDigits + "(\\.)?)|" +
     *
     *        // 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt
     *        "(0[xX]" + HexDigits + "?(\\.)" + HexDigits + ")" +
     *
     *        ")[pP][+-]?" + Digits + "))" +
     *       "[fFdD]?))" +
     *       "[\\x00-\\x20]*");// Optional trailing "whitespace"
     *
     *  if (Pattern.matches(fpRegex, myString))
     *      Double.valueOf(myString); // Will not throw NumberFormatException
     *  else {
     *      // Perform suitable alternative action
     *  }

All of that to say, that you don't have to worry about any of this if you let a converter do its usual parsing work without trying to short-circuit the process with an additional parsing step. IOW, don't parse input twice, we don't need verifiers.

Nits:

  • @param Javadoc tags should be listed first.
  • We don't need a new Java package: Let's keep it all in the original package.
  • Rebase on git master, we are now using the FinalParameters Checkstyle rule.

Claudenw and others added 3 commits January 11, 2024 11:45
Verifier interface became static class to hold common Predicate<String> instances for verification.
Moved EnumVerifier into Verifier as a static method.

All references to Verifier as a data type chagned to Predicate<String>.
@Claudenw Claudenw requested a review from garydgregory January 15, 2024 11:38
@Claudenw

Copy link
Copy Markdown
Contributor Author

@garydgregory I have added the Path to the standard converters handled by the TypeHandler and added documentation for the entire span of this change.

I also added a set of getOptionValue() methods that accept a Supplier to create the defaults.

@garydgregory garydgregory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep repeating it ;-) We don't need a "verifier". If later, this proves a requirement, we can discuss adding that concept. I still think that "verifying" is doomed to fail with both false positives and false negatives, on top of being redundant, since "verification" is done implicitly when parsing, for example by Double#parseDouble().

@Claudenw

Claudenw commented Jan 24, 2024

Copy link
Copy Markdown
Contributor Author

In working on Cassandra Stress test tool where we are trying to move to commons-cli there are cases where we want to quickly identify incorrect textual input without having to go through expensive conversion processes. This also gives an easy way to verify things like enum values and provide better error messages than if you just call enum.valueOf(str). Providing the option of the verifier means that often the converter is a simple call to an existing method.

I did remove the Verifier management code so we are not setting them by default and the TypeHandler does not set them either. Much like setting the type using the verifier is optional.

@Claudenw

Copy link
Copy Markdown
Contributor Author

@garydgregory I removed the verifier code and documentation.

@garydgregory garydgregory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Claudenw
Thank you for your updates. Please see my comments.

* Converts any exception except {@code UnsupportedOperationException} to a {@code ParseException}.
* if {@code e} is an instance of {@code ParseException} it is returned, otherwise a {@code ParseException} is
* created that wraps it.
* <p>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @Claudenw

  • Close HTML tags
  • Add @since tags to new public and protected elements.

if (e instanceof UnsupportedOperationException) {
throw (UnsupportedOperationException) e;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra white-space.


/**
* Registers custom {@code Converter}s with the {@code TypeHandler}.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @since tags to new public and protected elements.

*
* @param ch the specified character
* @return The class that {@code ch} represents
* @deprecated use {@link #getValueType(char)}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @Deprecated annotation to the method.

* @param ch the specified character
* @return The class that {@code ch} represents
*/
public static Class<?> getValueType(final char ch) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @since tags to new public and protected elements.

}

/**
* Registers a Converter for a Class. If @code converter} is null registration is cleared for {@code clazz}, and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one space after the ..

Comment thread src/site/xdoc/usage.xml
System.out.println("Unexpected exception:" + exp.getMessage());
}</source>
</section>
<section name="Converting (Parsing) Option Values">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 😄 👏

@Claudenw

Copy link
Copy Markdown
Contributor Author

@garydgregory requested changes made. Also found old 'verifier' documentation still in place and removed it.

@garydgregory garydgregory changed the title Fixes for CLI-321 withoug using BeanUtils Add and use a Converter interface and implementations without using BeanUtils Jan 29, 2024
@garydgregory garydgregory merged commit 9256a4d into apache:master Jan 29, 2024
@garydgregory garydgregory changed the title Add and use a Converter interface and implementations without using BeanUtils [CLI-321] Add and use a Converter interface and implementations without using BeanUtils Jan 29, 2024
*/
public static void resetConverters() {
converterMap.clear();
converterMap.put(Object.class, Converter.OBJECT);

@garydgregory garydgregory Jan 29, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Claudenw,
We have a mix of using constants and expressions to define this map. I think it would be better to do it one way or another: Use all constants or use all expressions.
The simplest would be to inline the constants from Converter and decrease the new public footprint. Alternatively, to keep constants we could make them package-private in a new or existing class.
Any thoughts on this?

@Claudenw Claudenw deleted the Functional_Converters branch February 19, 2024 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants